// Copyright 2012 Jay Young. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//      http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS-IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

/**
 * @fileoverview
 *
 * Parse an options file.
 */

var fs = require('fs'),
    strings = require('ringo/utils/strings'),
    logger = require('ringo/logging').getLogger('Config');



/**
 * A class whose sole responsibility is taking the command line options object
 * and processing it so that various parts of the program can use them
 * regardless of how they were specified (config file or command-line args).
 *
 * @param {Object} options The options object returned by args.parse().
 * @param {ringo.args.Parser} args The arguments parser.
 * @constructor
 */
var ConfigParser = exports.ConfigParser = function(options, args) {
  if (options.help) {
    this.usage(args);
    return;
  }
  
  if (options.test) {
    this.test = options.test;
    return;
  }

  if (! options.config) {
    throw Error('Config file must be provided via -c argument.');
  }
  var contents = fs.read(options.config)
                      .replace(/\/\/.*\n/g, '') // Remove line comments
                      .replace(/\/\*[\w\W]*?\*\//mg, ''); // Remove /* */

  var config = JSON.parse(contents);

  if ((! config.inputs) && (! config.paths)) {
    throw Error('Config file must include either "inputs" or "paths" ' +
                'properties.');
  }

  if (! config['jsdoc-html-output-path']) {
    throw Error('Config file must include "jsdoc-html-output-path" ' +
                'property.');
  }

  // Normalize {(undefined|null|string|Array.<string>)} options to always be
  // arrays (zero length, if need be);
  if (typeof config.inputs === 'string') {
    config.inputs = [config.inputs];
  }

  config.inputs = config.inputs || [];
  config.paths = config.paths || {};

  config.docmap =
      options.generate ? config['jsdoc-html-output-path'] + 'docmap.json' :
                         null;

  if (options.ast && config.docmap) {
    throw Error('Cannot print an AST when using a pre-built doc map file.');
  }
  
  if (config.parse && config.docmap) {
    throw Error('Conflicting modes specified (-p = parse-only, -g = use ' +
                'pre-built info from a previous parse.)');
  }

  /**
   * Re-document all files even if the doc-info file is newer than the source
   * file's last-modified time.
   *
   * @type {boolean}
   */
  this.force = !!options.force;

  /**
   * Flag indicating whether the program should parse the inputs, print the AST,
   * and quit.
   *
   * @type {boolean}
   */
  this.ast = !!options.ast;

  /**
   * Flag indicating whether the program should run with verbose output.
   *
   * @type {boolean}
   */
  this.verbose = !!options.verbose;

  var outputPath = fs.canonical(config['jsdoc-html-output-path']) + '/';

  /**
   * Path to the directory where documentation files should be dumped.
   *    Guaranteed to end in a trailing slash.
   *
   * @type {string}
   */
  this.outputPath = outputPath;
  require('rdoc/prep').prepare(this);

  /**
   * An array of inputs files.
   * @type {Array.<string>}
   */
  this.inputs = config.inputs;

  /**
   * An array of paths to search for input files.
   * @type {!Object.<{printName: string, filePrefix: string}>}
   */
  this.paths = config.paths;

  /**
   * List of paths and filenames to exclude from the parser inputs.
   *
   * @type {Array.<string>}
   * @private
   */
  this.excludes_ = config['rdoc-exclude'] || [];

  /**
   * List of path and filename prefixes to be checked against parser inputs.
   * Matches will be excluded.
   *
   * @type {Array.<string>}
   * @private
   */
  this.excludePrefixes_ = config['rdoc-exclude-prefix'] || [];

  /**
   * List of path and filename suffixes to be checked against parser inputs.
   * Matches will be excluded.
   *
   * @type {Array.<string>}
   * @private
   */
  this.excludeSuffixes_ = config['rdoc-exclude-suffix'] || [];

  /**
   * If the user specifies the -p ("parse") option, only parse the input files.
   * Do not generate the finished doc pages.
   *
   * @type {boolean}
   */
  this.parse = !!options.parse;
  
  /**
   * If the -g ("generate") option was specified, use this docmap.json file.
   * @type {?string}
   */
  this.usePrebuilt = config.docmap;

  /**
   * Set the size of the LRU cache used in @override processing.
   *
   * @type {number}
   */
  this.cacheSize = config['max-cache-size'] || 5000;
};


/**
 * Print the help text and exit.
 * @param {ringo.args.Parser} args The args parser.
 */
ConfigParser.prototype.usage = function(args) {
  var usage = require('rdoc/templates/usage');

  print(usage.usage());
  print(args.help() + '\n');
  print(usage.help());

  java.lang.System.exit(0);
};


/**
 * @return {Array.<string>} A list of all files to be parsed and documented.
 */
ConfigParser.prototype.getFileset = function() {
  logger.info('Generating list of inputs.');
  if (this.fileset_) {
    return this.fileset_;
  }

  var paths = Object.keys(this.paths),
      inputs = this.inputs,
      fileset = [];

  for (var i = 0, len = paths.length; i < len; ++i) {
    var path = paths[i],
        tree = fs.listTree(path);

    for (var j = 0, treeSize = tree.length; j < treeSize; ++j) {
      var file = path + tree[j];
      if (! this.excludePath_(file)) {
        fileset.push(file);
      }
    }
  }

  /**
   * The full list of input files.
   * @type {Array.<string>}
   * @private
   */
  this.fileset_ = fileset = fileset.concat(inputs);
  return fileset;
};


/**
 * Determines whether to exclude the given path based on any rdoc-exclude*
 * properties of the config object.
 *
 * @param {string} path The file name to check against the exclude lists.
 *
 * @return {boolean} True if the path should be excluded, false otherwise.
 * @private
 */
ConfigParser.prototype.excludePath_ = function(path) {
  var excludes = this.excludes_,
      excludePrefixes = this.excludePrefixes_,
      excludeSuffixes = this.excludeSuffixes_;

  if (! strings.endsWith(path, '.js')) {
    return true;
  }

  if (/\.svn/.test(path) || /\.hg/.test(path)) {
    return true;
  }

  if (excludes.some(function(excludePath) {
    if (path === excludePath) {
      return true;
    }

    var parts = fs.split(path);
    if (parts[parts.length - 1] === excludePath) {
      return true;
    }

    return false;
  })) { return true; }

  if (excludePrefixes.some(function(excludePathPrefix) {
    // If the full path begins with the prefix, return true.
    if (strings.startsWith(path, excludePathPrefix)) {
      return true;
    }

    // If just the filename begins with the prefix, return true.
    var parts = fs.split(path),
        filename = parts[parts.length - 1];
    if (strings.startsWith(fileName, excludePathPrefix)) {
      return true;
    }

    // Otherwise return false.
    return false;
  })) { return true; }

  if (excludeSuffixes.some(function(excludePathSuffix) {
    return strings.endsWith(path, excludePathSuffix);
  })) { return true; }

  return false;
};


/**
 * Parse the provided pre-built docmap.json file and return the de-serialized
 * object.
 *
 * @return {DocMap} A phony instance of DocMap.  Has all the data but none of
 *    the methods.
 */
ConfigParser.prototype.parsePrebuilt = function() {
  var docMap = this.usePrebuilt;
  logger.info('Parsing pre-built doc map file ' + docMap);
  return JSON.parse(fs.read(docMap));
};


/**
 * Given a JS source file, return the path/filename for its generated DocInfo
 * file.  This can be combined with DocInfo.fromFile to go from a source file
 * to the relevant DocInfo object.
 *
 * @param {string} filename The filename of the JS file being documented.
 * @return {{docFile: string, rev: ?string}} The filename where the
 *    documentation for that file should be saved.
 */
ConfigParser.prototype.getDocFile = function(filename) {
  var paths = Object.keys(this.paths),
      outputPath = this.outputPath;
  
  for (var i = 0, len = paths.length; i < len; ++i) {
    if (strings.startsWith(filename, paths[i])) {
      var path = paths[i],
          pathDesc = this.paths[path],
          pathName =
              pathDesc && pathDesc.filePrefix ? pathDesc.filePrefix + '-' : '',
          file = (filename.substr(path.length).replace(/\//g, '$')).slice(0, -3),
          output = outputPath + pathName + file + '.json';

      return {
        docFile: output,
        rev: pathDesc.rev
      };
    }
  }
  
  // If we've gotten here, there were no matches in the pathNames map.
  var file = (filename.substr(filename.lastIndexOf('/') + 1)).slice(0, -3),
      output = outputPath + file + '.json';

  return {
    docFile: output,
    rev: null
  };
};
